
#include <assert.h>
#include <stdio.h>
#include <string.h>


// Typedef

typedef enum { false, true } bool;
typedef unsigned char byte;
typedef unsigned short word;


// Utility

bool debug = false;

word SignExt(byte d) { return d + (0x01FE * (d & 0x80)); }
void ExchangeByte(byte* a, byte* b) { byte t = *a; *a = *b; *b = t; }
void ExchangeWord(word* a, word* b) { word t = *a; *a = *b; *b = t; }

bool streq(char* a, char* b) { for(;*a && *b; a++, b++) { if(*a != *b) return false; } return *a == *b; }


// RAM

#define RAM_SIZE 65536
byte RAM[RAM_SIZE];
byte RamRead(word addr) { assert(addr>=0 && addr<RAM_SIZE); return RAM[addr]; }
void RamWrite(word addr, byte data) { assert(addr>=0 && addr<RAM_SIZE); RAM[addr] = data; }


// Core

typedef enum {
	oper_brk, oper_nop, oper_int, oper_rti, oper_run, oper_hlt, oper_prn, oper_rts,
	oper_shr, oper_shl, oper_ror, oper_rol, oper_sra, oper_swn, oper_xxs, oper_xxi,
	oper_010, oper_011, oper_012, oper_013, oper_014, oper_015, oper_016, oper_017,
	oper_jpr, oper_019, oper_01A, oper_01B, oper_jpz, oper_jnz, oper_jpc, oper_jnc,
} CoreOper0;
typedef enum {
	oper_100, oper_101, oper_102, oper_103, oper_104, oper_105, oper_106, oper_107,
	oper_108, oper_109, oper_jmp, oper_jsr, oper_stf, oper_sta, oper_stx, oper_sty,
	oper_ldf, oper_lda, oper_ldx, oper_ldy, oper_adx, oper_sbx, oper_ady, oper_sby,
	oper_ads, oper_sbs, oper_add, oper_sub, oper_cmp, oper_and, oper_ior, oper_xor,
} CoreOper1;
typedef enum {
	mode_non, mode_xa, mode_ya, mode_ib, mode_xyi, mode_str, mode_iwi, mode_stp,
} CoreMode;

typedef struct {
	byte f;
	byte a;
	byte x;
	byte y;
	word sp;
	word pc;
	word iv;
	byte ins;
	byte ufg;
	byte run;
} CoreCPU;

void CoreInit(CoreCPU* c) { for(int i=0; i<sizeof(c); i++) { ((byte*)c)[i] = 0; } }

byte CoreGetFlags(CoreCPU* c) { return c->f | (c->ufg << 7) | (c->run << 6); }
void CoreSetFlags(CoreCPU* c, byte f) { c->f = f & 3; }

byte CoreNext(CoreCPU* c) { return RamRead(c->pc++); }
word CoreNextWord(CoreCPU* c) { return (CoreNext(c)<<8) | CoreNext(c); }
void CorePush(CoreCPU* c, byte v) { RamWrite(c->sp++, v); }
void CorePushWord(CoreCPU* c, word w) { CorePush(c, (byte)(w>>8)); CorePush(c, (byte)w); }
byte CorePop(CoreCPU* c) { return RamRead(--c->sp); }
word CorePopWord(CoreCPU* c) { return CorePop(c) | (CorePop(c)<<8); }

byte CoreAdd(CoreCPU* c, byte a, byte b, byte d) {
	word res = a + b + d;
	c->f = ((res>>8) & 1) | (((res & 0xFF)==0) << 1);
	return (byte)res;
}
void CoreSetZ(CoreCPU* c, byte res) { c->f = (c->f & ~2) | ((res==0) << 1); }
void CoreExec(CoreCPU* c) {
	if(c->ufg && (!c->run)) return;
	
	word oldpc = c->pc;
	c->ins = CoreNext(c);
	
	CoreOper0 oper0 = c->ins;
	if(oper0<=32) {
		switch(oper0) {
			       case oper_nop : ;
			break; case oper_brk : assert(c->ufg == true ); ExchangeWord(&c->pc, &c->iv); c->ufg = false;
			break; case oper_int : assert(c->ufg == true ); ExchangeWord(&c->pc, &c->iv); c->ufg = false;
			break; case oper_rti : assert(c->ufg == false); ExchangeWord(&c->pc, &c->iv); c->ufg = true ;
			break; case oper_run : assert(c->run == false); c->run = true ;
			break; case oper_hlt : assert(c->run == true ); c->run = false;
			break; case oper_prn : printf("%c", c->a);
			break; case oper_rts : c->pc = CorePopWord(c);
			break; case oper_shr : {                       c->f = (c->a     ) & 1; c->a = (c->a >> 1)                ; CoreSetZ(c, c->a); }
			break; case oper_shl : {                       c->f = (c->a >> 7) & 1; c->a = (c->a << 1)                ; CoreSetZ(c, c->a); }
			break; case oper_ror : { byte oldc = c->f & 1; c->f = (c->a     ) & 1; c->a = (c->a >> 1) | (oldc << 7  ); CoreSetZ(c, c->a); }
			break; case oper_rol : { byte oldc = c->f & 1; c->f = (c->a >> 7) & 1; c->a = (c->a << 1) | (oldc       ); CoreSetZ(c, c->a); }
			break; case oper_sra : {                       c->f = (c->a     ) & 1; c->a = (c->a >> 1) | (c->a & 0x80); CoreSetZ(c, c->a); }
			break; case oper_swn : c->f = 0; c->a = (c->a >> 4) | (c->a << 4); CoreSetZ(c, c->a);
			break; case oper_xxs :                  ExchangeByte(&c->x, (byte*)(&c->sp) + 1); ExchangeByte(&c->y, (byte*)(&c->sp));
			break; case oper_xxi : assert(!c->ufg); ExchangeByte(&c->x, (byte*)(&c->iv) + 1); ExchangeByte(&c->y, (byte*)(&c->iv));
			break; case oper_jpr :                       { byte offs = CoreNext(c); c->pc += SignExt(offs); }
			break; case oper_jpz : { if((c->f & 2) != 0) { byte offs = CoreNext(c); c->pc += SignExt(offs); } else { c->pc++; } }
			break; case oper_jnz : { if((c->f & 2) == 0) { byte offs = CoreNext(c); c->pc += SignExt(offs); } else { c->pc++; } }
			break; case oper_jpc : { if((c->f & 1) != 0) { byte offs = CoreNext(c); c->pc += SignExt(offs); } else { c->pc++; } }
			break; case oper_jnc : { if((c->f & 1) == 0) { byte offs = CoreNext(c); c->pc += SignExt(offs); } else { c->pc++; } }
			break; default: printf("invalid oper0 %i (opcode %02X) at %04X\n", oper0, c->ins, oldpc); assert(false);
		}
	} else {
		CoreOper1 oper1 = (c->ins)>>3;
		CoreMode mode = c->ins & 0x07;
		bool write = oper1>=oper_stf && oper1<=oper_sty;
		bool read  = mode>=mode_xyi && oper1>=oper_ldf;
		
		byte data = 0; word addr = 0;
		switch(mode) {
			       case mode_non: data = (oper1>=oper_adx && oper1<=oper_sub);
			break; case mode_xa : data = (oper1==oper_ldx || oper1==oper_adx || oper1==oper_sbx) ? c->a : c->x;
			break; case mode_ya : data = (oper1==oper_ldf || oper1==oper_ldy || (oper1>=oper_ady && oper1<=oper_sbs)) ? c->a : c->y;
			break; case mode_ib : data = CoreNext(c);
			break; case mode_xyi: addr = (c->x<<8) | c->y;
			break; case mode_str: addr = c->sp + CoreNext(c);
			break; case mode_iwi: addr = CoreNextWord(c);
			break; case mode_stp: addr = write ? (c->sp++) : (--c->sp);
		}
		if(read) { data = RamRead(addr); }
		
		switch(oper1) {
			       case oper_jmp: c->pc = addr;
			break; case oper_jsr: CorePushWord(c, c->pc); c->pc = addr;
			break; case oper_stf: RamWrite(addr, CoreGetFlags(c));
			break; case oper_sta: RamWrite(addr, c->a);
			break; case oper_stx: RamWrite(addr, c->x);
			break; case oper_sty: RamWrite(addr, c->y);
			break; case oper_ldf: CoreSetFlags(c, data);
			break; case oper_lda: c->a = data; c->f = 0; CoreSetZ(c, c->a);
			break; case oper_ldx: c->x = data; c->f = 0; CoreSetZ(c, c->x);
			break; case oper_ldy: c->y = data; c->f = 0; CoreSetZ(c, c->y);
			break; case oper_adx: c->x = CoreAdd(c, c->x,  data, 0);
			break; case oper_sbx: c->x = CoreAdd(c, c->x, ~data, 1);
			break; case oper_ady: c->y = CoreAdd(c, c->y,  data, 0);
			break; case oper_sby: c->y = CoreAdd(c, c->y, ~data, 1);
			break; case oper_ads: c->sp += data; c->f = 0; CoreSetZ(c, c->sp >> 8);
			break; case oper_sbs: c->sp -= data; c->f = 0; CoreSetZ(c, c->sp >> 8);
			break; case oper_add: c->a = CoreAdd(c, c->a,  data, 0);
			break; case oper_sub: c->a = CoreAdd(c, c->a, ~data, 1);
			break; case oper_cmp:        CoreAdd(c, c->a, ~data, 1);
			break; case oper_and: c->a &= data; c->f = 0; CoreSetZ(c, c->a);
			break; case oper_ior: c->a |= data; c->f = 0; CoreSetZ(c, c->a);
			break; case oper_xor: c->a ^= data; c->f = 0; CoreSetZ(c, c->a);
			break; default: printf("invalid oper1 %i (opcode %02X) at %04X\n", oper1, c->ins, oldpc); assert(false);
		}
	}
}


// Program loading
byte CharToHex (char c) {
	if     (c>='0' && c<='9') { return c-'0';    }
	else if(c>='a' && c<='f') { return c-'a'+10; }
	else if(c>='A' && c<='F') { return c-'A'+10; }
	else { assert(false); return 0; }
}
byte FNextHex(FILE* fi) {
	char c; do { c = fgetc(fi); } while(!( (c>='0' && c<='9') || (c>='a' && c<='f') || (c>='A' && c<='F') || feof(fi) ));
	return feof(fi) ? '0' : CharToHex(c);
}
byte FGetByte(FILE* fi, bool hex) {
	if(!hex) { return (byte)fgetc(fi); }
	else { return FNextHex(fi)<<4 | FNextHex(fi); }
}
word FGetWord(FILE* fi, bool hex) { return (FGetByte(fi, hex)<<8) | FGetByte(fi, hex); }
int LoadRam(char* fn) {
	bool hex = false; if(fn[0]=='-') { hex = true; fn++; }
	FILE* fi = streq(fn, "stdin") ? stdin : fopen(fn, "rb");
	if(!fi) { printf("Unable to open program file \"%s\"", fn); return 1; }
	
	while(!feof(fi)) {
		word addr = FGetWord(fi, hex);
		word len  = FGetWord(fi, hex);
		for(int i=0; i<len && !feof(fi); i++) {
			RamWrite(addr+i, FGetByte(fi, hex));
		}
	}
	
	fclose(fi);
	
	return 0;
}

// Main

CoreCPU core;

int main(int argc, char** argv) {
	if(argc<2) {
		printf("Specify a .bin file, i.e. core program.bin\n");
		return 1;
	}
	if(argc>=3 && streq(argv[2], "-d")) { debug = true; }
	
	// Initialize ram
	if(LoadRam(argv[1])) return 1;
	
	
	CoreCPU* c = &core;
	CoreInit(c); // init core
	if(debug) { printf("start\n"); }
	if(debug) { printf("f  a  x  y  sp   pc   iv   i  u  r  | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"); }
	while(true) {
		if(debug) { getc(stdin); }
		
		if(debug) {
			printf("%02X %02X %02X %02X %04X %04X %04X %02X %02X %02X | ", CoreGetFlags(c), c->a, c->x, c->y, c->sp, c->pc, c->iv, c->ins, c->ufg, c->run);
			for(int i=0x00; i<0x10; i++) { printf("%02X ", RamRead(i)); }
		}
		
		CoreExec(&core); // execute
		if(c->ufg && !c->run) { break; }
	}
	if(debug) { printf("done\n"); }
	
	return 0;
}
